/**
 * Copyright (c) 2018-2023, NXOS Development Team
 * SPDX-License-Identifier: Apache-2.0
 *
 * Contains: Implementation of virtio-input
 *
 * Change Logs:
 * Date           Author            Notes
 * 2023-08-04     Shiroko           Init
 */

#include "nxos.h"

#ifdef CONFIG_NX_DRIVER_VIRTIO_INPUT

#undef NX_LOG_MOD_NAME
#define NX_LOG_MOD_NAME "VIRTIO-INPUT"

#include "base/log.h"

#define DRV_NAME "virtio input"
#define DEV_NAME "virtio_input"

#include "../virtio.h"
#include "./virtio_input.h"

#define STRING_FALLBACK(p, fallback) ((p) == NX_NULL ? (fallback) : (p))

NX_PRIVATE NX_Error
VirtioInputDeviceConfigRead(NX_Device *device,
                            VirtioInputDeviceConfig *deviceCfg,
                            VirtioInputConfigSelect select,
                            NX_U8 sub_select,
                            NX_U8 **buffer,
                            NX_Offset offset,
                            NX_Bool isString,
                            NX_Size *outSize)
{
    NX_U8 size;
    deviceCfg->select     = select;
    deviceCfg->sub_select = sub_select;
    VirtioReadDeviceConfig(device, NX_OFFSET_OF_STRUCT(VirtioInputDeviceConfig, size), &size, sizeof(size));
    if (size == 0) return NX_EOK; // No information is available
    if (*buffer == NX_NULL)
    {
        *buffer = NX_MemAlloc(size + (isString == NX_True ? 1 : 0));
        if (*buffer == NX_NULL) return NX_ENOMEM;
    }
    VirtioReadDeviceConfig(device, offset, *buffer, size);
    if (isString == NX_True)
    {
        /*
         * Specification said strings provided by device do not include a NUL terminator.
         * But QEMU provides us the strings with NUL terminator.
         * For compatibility, we do append a NUL terminator to string.
         */
        ((char *) (*buffer))[size] = '\0';
    }
    if (outSize != NX_NULL)
        *outSize = size + (isString == NX_True ? 1 : 0);
    return NX_EOK;
}


NX_PRIVATE NX_Error VirtioInputQueueEventHandler(NX_Device *device, VirtioQueue *queue, NX_U32 idx)
{
    VirtioInputDeviceExtension *deviceExtension = VirtioGetDevExtension(device, VirtioInputDeviceExtension);
    NX_ASSERT(queue->desc[idx].len == sizeof(VirtioInputEvent));
    VirtioInputEvent *event = (VirtioInputEvent *) ((NX_UArch) NX_Phy2Virt(queue->desc[idx].addr));
    // NX_LOG_D("Event received. Type: 0x%X, Code:0x%X, Value: 0x%X.", event->type, event->code, event->value);
    if (deviceExtension->event_buffer)
    {
        NX_FifoWrite(deviceExtension->event_buffer, (NX_U8 *) event, sizeof(VirtioInputEvent));
    }
    VirtioQueuePutIntoAvailRing(queue, idx);
    VirtioSendNotification(device, VIRTIO_INPUT_QUEUE_EVENT, idx);
    return NX_EOK;
}

NX_PRIVATE void VirtioInputCleanUp(NX_Device *device);

NX_PRIVATE NX_Error VirtioInputInit(NX_Driver *driver, NX_Device *device)
{
    // Negotiate features & status
    VirtioDeviceInitialize(device, NX_NULL, 0);

    // Device-specified initialization
    VirtioQueueAlloc(device, 2);
    VirtioQueueInit(device, VIRTIO_INPUT_QUEUE_EVENT, CONFIG_NX_DRIVER_VIRTIO_SPLIT_QUEUE_SIZE);
    VirtioQueueInit(device, VIRTIO_INPUT_QUEUE_STATUS, CONFIG_NX_DRIVER_VIRTIO_SPLIT_QUEUE_SIZE);
    VirtioQueueRegisterNotifier(VirtioQueueGet(device, VIRTIO_INPUT_QUEUE_EVENT),
                                VirtioInputQueueEventHandler);

    // Driver OK
    VirtioDeviceDriverOK(device);

    NX_Error error = NX_EOK;

    VirtioInputDeviceExtension *deviceExtension = NX_MemAlloc(sizeof(VirtioInputDeviceExtension));
    if (deviceExtension == NX_NULL)
        goto nomem_failed;
    // Read device config
    NX_Size                 size       = 0;
    VirtioInputDeviceConfig *deviceCfg = VirtioGetDeviceConfig(device);
    // -- Read Name
    if ((error = VirtioInputDeviceConfigRead(
            device, deviceCfg, VIRTIO_INPUT_CFG_ID_NAME, 0,
            (NX_U8 **) &deviceExtension->deviceConfig.name,
            NX_OFFSET_OF_STRUCT(VirtioInputDeviceConfig, data.string),
            NX_True, &size)) != NX_EOK)
        goto failed;
    // -- Read Serial
    if ((error = VirtioInputDeviceConfigRead(
            device, deviceCfg, VIRTIO_INPUT_CFG_ID_SERIAL, 0,
            (NX_U8 **) &deviceExtension->deviceConfig.serial,
            NX_OFFSET_OF_STRUCT(VirtioInputDeviceConfig, data.string),
            NX_True, &size)) != NX_EOK)
        goto failed;
    // -- Read Properties bitmap
    if ((error = VirtioInputDeviceConfigRead(
            device, deviceCfg, VIRTIO_INPUT_CFG_PROP_BITS, 0,
            &deviceExtension->deviceConfig.propBitmap,
            NX_OFFSET_OF_STRUCT(VirtioInputDeviceConfig, data.bitmap),
            NX_False, &size)) != NX_EOK)
        goto failed;
    deviceExtension->deviceConfig.propBitmapSize = size;
    // -- Read Dev ids
    void *pData = (void *) &deviceExtension->deviceConfig.devIds;
    if ((error = VirtioInputDeviceConfigRead(
            device, deviceCfg, VIRTIO_INPUT_CFG_ID_DEVIDS, 0,
            (NX_U8 **) &pData,
            NX_OFFSET_OF_STRUCT(VirtioInputDeviceConfig, data.ids),
            NX_False, &size)) != NX_EOK)
        goto failed;

    /*
     * For QEMU, BusType is BUS_VIRTUAL, Vendor is 0x0627
     * Product is:
     *      0x0001 - Keyboard (Only have version 1)
     *      0x0002 - Mouse (Version 1 is no wheel, Version 2 is with wheel)
     *      0x0003 - Tablet (Version is the same of mouse)
     *      0x0003 - Multi-touch (Only have version 1)
     * We leave it to the end user to handle specific event and device type.
     */
    NX_LOG_I("Input device: %s. Serial: %s. DevIds: busType=0x%X, vendor=0x%X, product=0x%X, version=0x%X",
             STRING_FALLBACK(deviceExtension->deviceConfig.name, "NO NAME"),
             STRING_FALLBACK(deviceExtension->deviceConfig.serial, "NO SERIAL"),
             deviceExtension->deviceConfig.devIds.busType,
             deviceExtension->deviceConfig.devIds.vendor,
             deviceExtension->deviceConfig.devIds.product,
             deviceExtension->deviceConfig.devIds.version
    );

    VirtioGetExtension(device)->devExt = (void *) deviceExtension;

    // Populate the event queue
    VirtioInputEvent *events = NX_MemAlloc(sizeof(VirtioInputEvent) * CONFIG_NX_DRIVER_VIRTIO_SPLIT_QUEUE_SIZE);
    if (events == NX_NULL)
        goto nomem_failed;
    else
    {
        deviceExtension->events = events;
        VirtioQueue *eventq     = VirtioQueueGet(device, VIRTIO_INPUT_QUEUE_EVENT);
        for (int    i           = 0; i < CONFIG_NX_DRIVER_VIRTIO_SPLIT_QUEUE_SIZE; i++)
        {
            NX_U32 idx = VirtioQueueDescAlloc(eventq);
            if (idx == VIRTIO_QUEUE_DESC_INVALID_ID) break;
            VirtioQueueDescFill(eventq, idx, (NX_U64) ((NX_UArch) NX_Virt2Phy(&events[i])), sizeof(VirtioInputEvent),
                                VIRTIO_QUEUE_DESC_FLAG_WRITE, 0);
            VirtioQueuePutIntoAvailRing(eventq, idx);
            eventq->trace[idx].needNotify = NX_True;
        }
        VirtioSendNotification(device, VIRTIO_INPUT_QUEUE_EVENT, VIRTIO_QUEUE_DESC_INVALID_ID);
    }
    return NX_EOK;

nomem_failed:
    error = NX_ENOMEM;
failed:
    VirtioInputCleanUp(device);
    return error;
}

NX_PRIVATE void VirtioInputCleanUp(NX_Device *device)
{
    // Reset the device
    VirtioDeviceReset(device);
    // Cleanup VirtioQueue
    for (int i = 0; i < VirtioGetExtension(device)->queue_num; i++)
        VirtioQueueDestroy(device, i);
    VirtioQueueFree(device);

    VirtioInputDeviceExtension *deviceExtension = VirtioGetDevExtension(device, VirtioInputDeviceExtension);
    if (deviceExtension == NX_NULL) return;
    if (deviceExtension->deviceConfig.name) NX_MemFree(deviceExtension->deviceConfig.name);
    if (deviceExtension->deviceConfig.serial) NX_MemFree(deviceExtension->deviceConfig.serial);
    if (deviceExtension->deviceConfig.propBitmap) NX_MemFree(deviceExtension->deviceConfig.propBitmap);
    if (deviceExtension->events) NX_MemFree(deviceExtension->events);
    NX_MemFree(deviceExtension);
}

NX_PRIVATE NX_Error open(struct NX_Device *device, NX_U32 flags)
{
    VirtioInputDeviceExtension *deviceExtension = VirtioGetDevExtension(device, VirtioInputDeviceExtension);
    if (deviceExtension->event_buffer != NX_NULL)
        return NX_EBUSY;
    deviceExtension->event_buffer = NX_FifoCreate(
            CONFIG_NX_DRIVER_VIRTIO_INPUT_EVENT_BUFFER_SIZE * sizeof(VirtioInputEvent));
    if (deviceExtension->event_buffer == NX_NULL)
        return NX_ENOMEM;
    return NX_EOK;
}

NX_PRIVATE NX_Error close(struct NX_Device *device)
{
    VirtioInputDeviceExtension *deviceExtension = VirtioGetDevExtension(device, VirtioInputDeviceExtension);
    if (deviceExtension->event_buffer == NX_NULL) return NX_EINVAL;
    NX_FifoDestroy(deviceExtension->event_buffer);
    deviceExtension->event_buffer = NX_NULL;
    return NX_EOK;
}

NX_PRIVATE NX_Error read(struct NX_Device *device, void *buf, NX_Offset off, NX_Size len, NX_Size *outLen)
{
    VirtioInputDeviceExtension *deviceExtension = VirtioGetDevExtension(device, VirtioInputDeviceExtension);

    if (len != sizeof(VirtioInputEvent)) return NX_EINVAL;
    if (NX_FifoLen(deviceExtension->event_buffer) <= 0) return NX_ENORES;
    *outLen = NX_FifoRead(deviceExtension->event_buffer, buf, sizeof(VirtioInputEvent));
    if (*outLen == 0) return NX_ENORES;
    else if(*outLen != 0 && *outLen != len) {
        return NX_EFAULT;
    }
    return NX_EOK;
}

NX_PRIVATE NX_Error write(struct NX_Device *device, void *buf, NX_Offset off, NX_Size len, NX_Size *outLen)
{
    return NX_ENOFUNC;
}

NX_PRIVATE NX_Error control(struct NX_Device *device, NX_U32 cmd, void *arg)
{
    VirtioInputDeviceExtension *deviceExtension = VirtioGetDevExtension(device, VirtioInputDeviceExtension);
    switch (cmd)
    {
    case VIRTIO_INPUT_CMD_RESET:
        NX_FifoReset(deviceExtension->event_buffer);
    case VIRTIO_INPUT_CMD_SET_STATUS:
    {
        if (arg == NX_NULL) return NX_EINVAL;
        VirtioInputEvent *status  = VirtioVirt2Phy(arg);
        VirtioQueue      *statusq = VirtioQueueGet(device, VIRTIO_INPUT_QUEUE_STATUS);
        NX_U32           idx      = VirtioQueueDescAlloc(statusq);
        if (idx == VIRTIO_QUEUE_DESC_INVALID_ID) return NX_ENORES;
        VirtioQueueDescFill(statusq, idx, (NX_U64) ((NX_UArch) status), sizeof(VirtioInputEvent), 0, 0);
        VirtioQueuePutIntoAvailRing(statusq, idx);
        VirtioSendNotification(device, VIRTIO_INPUT_QUEUE_STATUS, idx);
        VirtioWaitNotification(device, VIRTIO_INPUT_QUEUE_STATUS, idx);
        break;
    }
    default:
        if (cmd < VIRTIO_INPUT_CMD_GET_NAME || cmd > VIRTIO_INPUT_CMD_GET_ABS_INFO) return NX_ENOFUNC;
        if (arg == NX_NULL) return NX_EINVAL;
        VirtioInputGetRequest *req = (VirtioInputGetRequest *) VirtioVirt2Phy(arg);
        if (req->bufferSize == 0) return NX_EINVAL;
        if (req->buffer == NX_NULL) return NX_EINVAL;
        NX_U8 *buf = VirtioVirt2Phy(req->buffer);
        switch (cmd)
        {
        case VIRTIO_INPUT_CMD_GET_NAME:
        case VIRTIO_INPUT_CMD_GET_SERIAL:
        {
            char *src = cmd == VIRTIO_INPUT_CMD_GET_NAME ? deviceExtension->deviceConfig.name
                                                         : deviceExtension->deviceConfig.serial;
            if (src == NX_NULL)
            {
                buf[0] = '\0';
                break;
            }
            NX_Size len = 0;
            if ((len = NX_StrLen(src)) > req->bufferSize) return NX_ENOMEM;
            NX_StrCopyN((char *) buf, src, req->bufferSize);
            req->readSize = len;
            break;
        }
        case VIRTIO_INPUT_CMD_GET_PROP_BITMAP:
            if (req->bufferSize < deviceExtension->deviceConfig.propBitmapSize) return NX_ENOMEM;
            NX_MemCopy(buf, deviceExtension->deviceConfig.propBitmap, deviceExtension->deviceConfig.propBitmapSize);
            req->readSize = deviceExtension->deviceConfig.propBitmapSize;
            break;
        case VIRTIO_INPUT_CMD_GET_DEVIDS:
            if (req->bufferSize != sizeof(VirtioInputDevIds)) return NX_EINVAL;
            NX_MemCopy(buf, &deviceExtension->deviceConfig.devIds, sizeof(VirtioInputDevIds));
            req->readSize = sizeof(VirtioInputDevIds);
            break;
        case VIRTIO_INPUT_CMD_GET_EV_BITMAP:
        case VIRTIO_INPUT_CMD_GET_ABS_INFO:
        {
            NX_Error error  = NX_EOK;
            NX_Size  size   = 0;
            void     *pData = NX_NULL;
            if ((error = VirtioInputDeviceConfigRead(
                    device, VirtioGetDeviceConfig(device),
                    cmd == VIRTIO_INPUT_CMD_GET_EV_BITMAP ?
                    VIRTIO_INPUT_CFG_EV_BITS :
                    VIRTIO_INPUT_CFG_ABS_INFO,
                    req->selector,
                    (NX_U8 **) &pData,
                    (NX_Offset) (cmd == VIRTIO_INPUT_CMD_GET_EV_BITMAP ?
                                 NX_OFFSET_OF_STRUCT(VirtioInputDeviceConfig, data.bitmap) :
                                 NX_OFFSET_OF_STRUCT(VirtioInputDeviceConfig, data.abs)),
                    NX_False, &size)) != NX_EOK)
                return error;
            if (size > req->bufferSize)
            {
                NX_MemFree(pData);
                return NX_ENOMEM;
            }
            req->readSize = size;
            NX_MemCopy(buf, pData, size);
            NX_MemFree(pData);
            break;
        }
        default:
            return NX_ENOFUNC;
        }
        break;
    }
    return NX_EOK;
}

NX_PRIVATE NX_Error poll(struct NX_Device *device, NX_PollState *pState)
{
    VirtioInputDeviceExtension *deviceExtension = VirtioGetDevExtension(device, VirtioInputDeviceExtension);
    return NX_FifoPoll(deviceExtension->event_buffer, pState);
}

NX_PRIVATE NX_DriverOps virtioInputDriverOps = {
        .open = open,
        .close = close,
        .read = read,
        .write = write,
        .control = control,
        .poll = poll
};


NX_PRIVATE VirtioDriverTableEntity virtioInputDriverTableEntity = {
        .deviceId = VIRTIO_DEVICE_ID_INPUT,
        .driverName = "virtio-input",
        .deviceNameTemplate = "virtio_input_%d",
        .deviceType = NX_DEVICE_TYPE_INPUT,
        .driverOps = &virtioInputDriverOps,
        .initHandler = VirtioInputInit,
        .cleanUpHandler = VirtioInputCleanUp
};

VirtioDriverTableEntity *VirtioDriverTableEntityForVirtioInput(void)
{
    return &virtioInputDriverTableEntity;
}

#else

#include "../virtio.h"

VirtioDriverTableEntity *VirtioDriverTableEntityForVirtioInput(void)
{
    return NX_NULL;
}

#endif // CONFIG_NX_DRIVER_VIRTIO_INPUT
